﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using FinanSoft.Entities;
using FinanSoft.Utilities;
using System.Collections.ObjectModel;
using FinanSoft.DataLayer;

namespace FinanSoft.BusinessLayer
{
    class PeriodManager
    {

        #region Atributes
    
        private AccountingPeriod[] _accountingPeriod;
        private bool _isError;
        private String _errorMessage;
        private SeasonStatus[] _seasonStatus;
        #endregion

        #region Singleton


        private PeriodManager()
        {
            _isError = false;
            _errorMessage = "";
            _accountingPeriod = PeriodDataManager.GetInstance().GetAccountingPeriod();
            _seasonStatus = PeriodDataManager.GetInstance().GetSeasonStatus();
            if (_accountingPeriod == null)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_ACCOUNTING_PERIOD_READING;
            }
            else if (_seasonStatus == null)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_DB_CONNECTION_FAILED_STATUS_SEASON;
            }
            else if (_seasonStatus.Length == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_MISSING_SEASON_STATUS;
            }
            else
                _isError = false;
        }


        /// <summary>
        /// Única instancia de esta clase
        /// </summary>
        private static PeriodManager _instance;

        /// <summary>
        /// Método para implementar el patrón de Singleton
        /// </summary>
        /// <returns>Instancia única de la clase</returns>
        public static PeriodManager GetInstance()
        {
            if (_instance == null)
                _instance = new PeriodManager();
            return _instance;
        }
        #endregion

        #region Properties
        public bool IsError
        {
            get 
            {
                bool error = _isError;
                _isError = false;
                return error;
            }
        }

        public String ErrorMessage
        {
            get
            {
                _isError = false;
                String errorMsg = _errorMessage;
                _errorMessage = "";
                return errorMsg;
            }
        }
        #endregion

        #region Métodos

        /// <summary>
        /// Devuelve los estados de temporadas. Deberían ser siempre 3. Si tiene 0 elementos o devuelve nulo, hubo algún error.
        /// </summary>
        /// <returns>Devuelve los estados de temporadas. Puede ir nulo en caso de error</returns>
        public SeasonStatus[] GetSeasonStatus()
        {
            if (_seasonStatus == null)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_DB_CONNECTION_FAILED_STATUS_SEASON;
            }
            else if (_seasonStatus.Length == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_MISSING_SEASON_STATUS;
            }
            return _seasonStatus;
        }

        public AccountingPeriod GetCurrentAccountingPeriod()
        {
            AccountingPeriod[] periods = GetAccountingPeriod();
            DateTime today = DateTime.Now;
            foreach (AccountingPeriod period in periods)
            {
                if (period.StartDate < today && period.EndDate > today)
                    return period;
            }
            return null;
        }

        /// <summary>
        /// Retorna un objeto de estado de temporada mediante el nombre
        /// </summary>
        /// <param name="name">Nombre del estado: Abierto, Abierto excepto ventas, Cerrado</param>
        /// <returns>Estado de temporada asociado. Si no hay ningún estado de temporada asociado devuelve nulo</returns>
        public SeasonStatus GetSeasonStatus(String name)
        {
            foreach (SeasonStatus status in _seasonStatus)
            {
                if (status.Name.Equals(name))
                    return status;
            }
            return null;
        }

        /// <summary>
        /// Retorna un objeto de estado de temporada mediante el identificador
        /// </summary>
        /// <param name="id">Id del estado que se desea</param>
        /// <returns>Estado de temporada asociado. Si no hay ningún estado de temporada asociado devuelve nulo</returns>
        public SeasonStatus GetSeasonStatus(int id)
        {
            foreach (SeasonStatus status in _seasonStatus)
            {
                if (status.Id == id)
                    return status;
            }
            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="periodId"></param>
        /// <returns></returns>
        public bool MakeIncomeStatementEntries(int periodId)
        {
            int seasonId = GetPeriod(periodId).GetLastSeason().Id;
            List<Account> dividendAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.dividends);
            List<Account> incomeTaxAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.incomeTax);
            List<Account> dividendToPayAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.dividiendsToPay);
            List<Account> incomeTaxToPayAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.incomeTaxToPay);
            if (dividendAccountList.Count == 0 || dividendToPayAccountList.Count == 0 || incomeTaxAccountList.Count == 0 || incomeTaxToPayAccountList.Count == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_INCOME_STATEMENT_ENTRIES_ACC_MISSING;
                return false;
            }
            else if (GetPeriod(periodId).Seasons.Length == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_NO_SEASONS_ASSOCIATED;
                return false;
            }
            else
            {
                Account dividendAccount = dividendAccountList[0];
                Account dividendToPayAccount = dividendToPayAccountList[0];
                Account incomeTaxAccount = incomeTaxAccountList[0];
                Account incomeTaxToPayAccount = incomeTaxToPayAccountList[0];
                List<AccountingEntryDetail> dividendDetails = new List<AccountingEntryDetail>();
                List<AccountingEntryDetail> incomeTaxDetails = new List<AccountingEntryDetail>();
                // Obtenemos las utilidades retenidas antes de impuestos
                double[] retainedEarnings = getRetainedEarningsBeforeTaxes();
                // Aplicamos los asientos de impuesto de renta e impuesto de renta por pagar
                double taxPercentage = ReportManager.GetInstance().LoadedCompany.TaxPercentage;
                double localTax = retainedEarnings[Constants.LOCAL_AMOUNT_INDEX]*taxPercentage;
                double systemTax = retainedEarnings[Constants.SYSTEM_AMOUNT_INDEX]*taxPercentage;
                incomeTaxDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            incomeTaxAccount.AccountID, systemTax, localTax,
                            Constants.AccountingEntryType.debit, Constants.INVALID_ID));
                incomeTaxDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            incomeTaxToPayAccount.AccountID, systemTax, localTax,
                            Constants.AccountingEntryType.credit, Constants.INVALID_ID));
                // Guardamos el asiento de impuestos
                if (!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, seasonId, Constants.INCOME_STATEMENT_ENTIRES_REFERENCE_MSG, "", "", incomeTaxDetails))
                {
                    _isError = true;
                    _errorMessage = Constants.ERROR_INCOME_TAX_TO_PAY_FAILED;
                    return false;
                }
                // Aplicamos los asientos de dividendos y dividendos por pagar
                double dividendsPercentage = ReportManager.GetInstance().LoadedCompany.DividendPercentage;
                double localDividends = retainedEarnings[Constants.LOCAL_AMOUNT_INDEX] * (1 - taxPercentage) * dividendsPercentage;
                double systemDividends = retainedEarnings[Constants.SYSTEM_AMOUNT_INDEX] * (1 - taxPercentage) * dividendsPercentage;
                dividendDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            dividendAccount.AccountID, systemDividends, localDividends,
                            Constants.AccountingEntryType.debit, Constants.INVALID_ID));
                dividendDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            dividendToPayAccount.AccountID, systemDividends, localDividends,
                            Constants.AccountingEntryType.credit, Constants.INVALID_ID));
                // Guardamos el asiento de dividendos
                if (!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, seasonId, Constants.INCOME_STATEMENT_ENTIRES_REFERENCE_MSG2, "", "", dividendDetails))
                {
                    _isError = true;
                    _errorMessage = Constants.ERROR_DIVIDENDS_TO_PAY_FAILED;
                    return false;
                }
                return true;
            }
        }

        /// <summary>
        /// Obtiene las utilidades retenidas según el catálogo contable actual
        /// </summary>
        /// <returns>Devuelve un arreglo que contiene las utilidades retenidas en moneda local y del sistema. La
        /// posición de ellas está dada por SYSTEM_AMOUNT_INDEX y LOCAL_AMOUNT_INDEX</returns>
        private double[] getRetainedEarningsBeforeTaxes()
        {
            double[] retainedEarnings = {0, 0};
            List<Account> accounts = CatalogManager.GetInstance().GetAccountingCatalog().GetActiveAccounts();            
            Constants.AccountType[] admittedTypes = { Constants.AccountType.income, Constants.AccountType.oincome, Constants.AccountType.outcome, Constants.AccountType.ooutcome };
            List<AccountingEntryDetail> profitLossDetails = new List<AccountingEntryDetail>();
            // Creo una linea para el asiento con cada ingreso y gasto
            foreach (Account account in accounts)
            {
                if (admittedTypes.Contains(account.AccountType) && account.SpecialType != Constants.SpecialType.dividends && account.SpecialType != Constants.SpecialType.incomeTax)
                {
                    double amountMultiplier = account.AlDebe ? -1 : 1;
                    retainedEarnings[Constants.SYSTEM_AMOUNT_INDEX] += account.SysAmount * amountMultiplier;
                    retainedEarnings[Constants.LOCAL_AMOUNT_INDEX] += account.LocalAmount * amountMultiplier;
                }
            }
            return retainedEarnings;
        }

        /// <summary>
        /// Hace el cierre contable para un periodo en específico. Esto solo incluye dejar los ingresos y gastos en 0, sumar esto
        /// con la cuenta de pérdidas y ganancias a la cuenta de utilidades retenidas y restar los dividendos a las utilidades retenidas.
        /// El cambio de estado a las temporadas NO se realiza en este método.
        /// </summary>
        /// <param name="periodId">Id del periodo que se está cerrando</param>
        /// <returns></returns>
        public bool MakeAccountingClose(int periodId)
        {
            int seasonId = GetPeriod(periodId).GetLastSeason().Id;
            List<Account> profitLossAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.profitAndLoss); 
            List<Account> retainedEarningsAccountList = CatalogManager.GetInstance().GetAccountingCatalog().GetSpecialAccounts(Constants.SpecialType.retainedEarnings);
            if (profitLossAccountList.Count == 0 || retainedEarningsAccountList.Count == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_ACCOUNTING_CLOSE_ACC_MISSING;
                return false;
            }
            else if (GetPeriod(periodId).Seasons.Length == 0)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_NO_SEASONS_ASSOCIATED;
                return false;
            }
            else
            {
                Account profitLossAccount = profitLossAccountList[0];
                Account retainedEarningsAccount = retainedEarningsAccountList[0];
                List<Account> accounts = CatalogManager.GetInstance().GetAccountingCatalog().GetActiveAccounts();
                double debitSystemAmount = 0;
                double debitLocalAmount = 0;
                Constants.AccountType[] admittedTypes = { Constants.AccountType.income, Constants.AccountType.oincome, Constants.AccountType.outcome, Constants.AccountType.ooutcome };
                List<AccountingEntryDetail> profitLossDetails = new List<AccountingEntryDetail>();
                // Creo una linea para el asiento con cada ingreso y gasto
                foreach (Account account in accounts)
                {
                    if (admittedTypes.Contains(account.AccountType) && account.SpecialType != Constants.SpecialType.dividends && account.LocalAmount > 0)
                    {
                        double amountMultiplier = account.AlDebe ? 1 : -1;
                        debitSystemAmount += account.SysAmount * amountMultiplier;
                        debitLocalAmount += account.LocalAmount * amountMultiplier;
                        profitLossDetails.Add(emptyAccount(account));
                    }
                }
                // Creo una linea para la cuenta de pérdidas y ganancias
                bool alDebe = debitLocalAmount > 0;
                if (!alDebe)
                {
                    debitLocalAmount *= -1;
                    debitSystemAmount *= -1;
                }
                profitLossDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            profitLossAccount.AccountID, debitSystemAmount, debitLocalAmount,
                            alDebe ? Constants.AccountingEntryType.debit : Constants.AccountingEntryType.credit, Constants.INVALID_ID));
                // Creo el asiento contable para las pérdidas y ganancias
                if (!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, seasonId, Constants.ACCOUNTING_CLOSE_REFERENCE_MSG, "", "", profitLossDetails))
                {
                    _isError = true;
                    _errorMessage = Constants.ERROR_PROFIT_AND_LOSS_FAILED;
                    return false;
                }
                // Hacemos el asiento de utilidades retenidas contra pérdidas y ganancias
                alDebe = profitLossAccount.AlDebe;
                List<AccountingEntryDetail> retainedEarningsDetails = new List<AccountingEntryDetail>();
                retainedEarningsDetails.Add(emptyAccount(profitLossAccount));
                retainedEarningsDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                        retainedEarningsAccount.AccountID, profitLossAccount.SysAmount, profitLossAccount.LocalAmount,
                        alDebe ? Constants.AccountingEntryType.debit : Constants.AccountingEntryType.credit, Constants.INVALID_ID));
                if(!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, seasonId, Constants.ACCOUNTING_CLOSE_REFERENCE_MSG2, "", "", retainedEarningsDetails))
                {
                    _isError = true;
                    _errorMessage = Constants.ERROR_RETAINED_EARNINGS_FAILED;
                    return false;
                }
                // Hacemos el asiento de utilidades retenidas contra dividendos
                debitSystemAmount = 0;
                debitLocalAmount = 0;
                List<AccountingEntryDetail> dividendsDetails = new List<AccountingEntryDetail>();
                // Creo una linea para el asiento con cuenta de dividendos
                foreach (Account account in accounts)
                {
                    if (account.SpecialType == Constants.SpecialType.dividends)
                    {
                        double amountMultiplier = account.AlDebe ? 1 : -1;
                        debitSystemAmount += account.SysAmount * amountMultiplier;
                        debitLocalAmount += account.LocalAmount * amountMultiplier;
                        dividendsDetails.Add(emptyAccount(account));
                    }
                }
                alDebe = debitLocalAmount > 0;
                if (!alDebe)
                {
                    debitLocalAmount *= -1;
                    debitSystemAmount *= -1;
                }
                dividendsDetails.Add(
                    AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                            retainedEarningsAccount.AccountID, debitSystemAmount, debitLocalAmount,
                            alDebe ? Constants.AccountingEntryType.debit : Constants.AccountingEntryType.credit, Constants.INVALID_ID));
                // Creo el asiento contable para las pérdidas y ganancias
                if(!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, seasonId, Constants.ACCOUNTING_CLOSE_REFERENCE_MSG, "", "", dividendsDetails))
                {
                    _isError = true;
                    _errorMessage = Constants.ERROR_DIVIDENDS_FAILED;
                    return false;
                }
                return true;
            }
        }

        /// <summary>
        /// Hace un detalle de asiento para vaciar el saldo de esta cuenta. Es decir que si la cuenta está al debe entonces
        /// el detalle será por un monto igual al saldo de la cuenta y al haber.
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        private AccountingEntryDetail emptyAccount(Account account)
        {
            return AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                        account.AccountID, account.SysAmount, account.LocalAmount,
                        account.AlDebe ? Constants.AccountingEntryType.credit : Constants.AccountingEntryType.debit, Constants.INVALID_ID);
        }

        /// <summary>
        /// Método que se encarga de agregar un nuevo periodo
        /// </summary>
        /// <param name="mode">Modalidad del Period</param>
        /// <param name="nombre">Nombre del Periodo</param>
        /// <param name="FechaInicio">Fecha con que inicia el periodo</param>
        /// <param name="FechaFin">Fecha con que termina el periodo</param>
        /// <param name="temporadas">Arreglo de temporadas del periodo</param>
        /// <returns>Retorna booleano de éxito o no</returns>
        public bool CreateNewPeriod(int mode, string name, DateTime FechaInicio, DateTime FechaFin, Season[] Temporadas)
        {
            AccountingPeriod newPeriod = new AccountingPeriod(Constants.INVALID_ID, mode, name, FechaInicio, FechaFin, Temporadas);
            
            bool noErrorDB = PeriodDataManager.GetInstance().CreateNewPeriod(newPeriod);
            if (!noErrorDB)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_NEW_ACCOUNT_FAILED;

              
                return false;
            }
            return true;
        }

        /// <summary>
        /// Método que se encarga de inicializar los periodos
        /// </summary>
        /// <returns>Retorna todos los periodos contables con las temporadas</returns>
        public AccountingPeriod[] GetAccountingPeriod()
        {

            if (_accountingPeriod == null)
            {
                _isError = true;
                Console.WriteLine("error y algo mas");
                _errorMessage = Constants.ERROR_ACCOUNTING_PERIOD_READING;
                
            }
            _accountingPeriod = PeriodDataManager.GetInstance().GetAccountingPeriod();
            return _accountingPeriod;
        }

        /// <summary>
        /// Metodo que se encarga de guardarme una temporada.
        /// </summary>
        /// <param name="period">El periodo al que pertenece la temporada</param>
        /// <param name="name">Nombre de la Temporada</param>
        /// <param name="StartDate">Fecha con que inicia el periodo</param>
        /// <param name="EndDate">Fecha con que termina el periodo</param>
        /// <param name="InitialDueDate">Fecha del inicio del vencimiento del periodo</param>
        /// <param name="EndDueDate">Fecha del fin del vencimiento del periodo</param>
        /// <returns>retorna verdadero si se guardó con éxito</returns>
        public bool CreateNewSeason(int period, string name, DateTime StartDate, DateTime EndDate,DateTime InitialDueDate, DateTime EndDueDate)
        {
            Season newSeason = new Season(Constants.INVALID_ID, name, period, 0, StartDate, EndDate, InitialDueDate, EndDueDate);


            bool noErrorDB = PeriodDataManager.GetInstance().CreateNewSeason(newSeason);
            if (!noErrorDB)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_NEW_SEASON_FAILED;


                return false;
            }
            return true;
        }

        /// <summary>
        /// Método que se encarga de modificar el estado de una temporada
        /// </summary>
        /// <param name="temporada">Es un objeto temporada sobre el cual se cambiara el estado</param>
        /// <param name="status">El nuevo estado que se va a cambiar</param>
        /// <returns>retorna exito si se actualizó correctamente</returns>
        public bool ModifyStatusSeason(Season temporada, int status)
        {
            Season newSeason = new Season(temporada.Id, temporada.Name, temporada.Period, status, temporada.StartDate, temporada.EndDate, temporada.InitialDueDate, temporada.EndDueDate);


            bool noErrorDB = PeriodDataManager.GetInstance().ModifyStatusSeason(newSeason);
            if (!noErrorDB)
            {
                _isError = true;
                _errorMessage = Constants.ERROR_MOD_SEASON_FAILED;


                return false;
            }
            return true;
        }

        /// <summary>
        /// Funcion que me devuelve un objeto periodo con su id respectivo
        /// </summary>
        /// <param name="idPeriod">Id del periodo a modificar</param>
        /// <returns>Me retorna el objeto periodo</returns>
        public AccountingPeriod GetPeriod(int idPeriodo)
        {
            AccountingPeriod[] arregloPeriodos = GetAccountingPeriod();
            AccountingPeriod periodo = new AccountingPeriod();
            for (int k = 0; k < arregloPeriodos.Length; k++)
            {
                if (arregloPeriodos[k].Id == idPeriodo)
                {
                    periodo.Id = arregloPeriodos[k].Id;
                    periodo.Type = arregloPeriodos[k].Type;
                    periodo.Name = arregloPeriodos[k].Name;
                    periodo.StartDate = arregloPeriodos[k].StartDate;
                    periodo.EndDate = arregloPeriodos[k].EndDate;
                    periodo.Seasons = arregloPeriodos[k].Seasons;
                    return periodo;
                }
                else { }
            }
            return periodo;
        }

        /// <summary>
        /// Funcion que me devuelve un objeto temporada con su id respectivo
        /// </summary>
        /// <param name="idSeason">Id de la temporada</param>
        /// <returns>Me retorna el objeto temporada</returns>
        public Season GetSeason(int idSeason)
        {
            AccountingPeriod[] arregloPeriodo = GetAccountingPeriod();
            foreach(AccountingPeriod period in arregloPeriodo)
                foreach (Season season in period.Seasons)
                    if (season.Id == idSeason)
                        return season;
            return null;
        }

        #endregion

    }
}
